在 TypeScript 專案裡,錯誤處理常常是最鬆的地方:
catch
到的 err
永遠是 any
throw
,前端只好亂猜內容今天我們用三招,把錯誤處理變得 可預期、可補全、可檢查。
假設後端用 Express(Day 15–18 範例),我們先定義一個錯誤回應型別:
// src/types/api.ts
export type ApiError = {
code: string; // 錯誤代碼,例如 "USER_NOT_FOUND"
message: string; // 錯誤描述
details?: unknown; // 可選,放更多資訊
};
在 Express 中統一處理:
// src/middleware/errorHandler.ts
import { Request, Response, NextFunction } from "express";
import type { ApiError } from "../types/api";
export function errorHandler(err: any, req: Request, res: Response, next: NextFunction) {
console.error(err);
const apiError: ApiError = {
code: err.code || "INTERNAL_ERROR",
message: err.message || "Something went wrong",
details: err.details,
};
res.status(err.status || 500).json(apiError);
}
在路由中用 throw
:
router.get("/:id", async (req, res, next) => {
try {
const user = await prisma.user.findUnique({ where: { id: req.params.id } });
if (!user) {
const err = new Error("User not found");
(err as any).code = "USER_NOT_FOUND";
(err as any).status = 404;
throw err;
}
res.json(user);
} catch (e) {
next(e);
}
});
Result
型別模式(推薦)用 Result
讓成功與失敗用 同一型別 表示:
// src/types/result.ts
export type Result<T, E> =
| { ok: true; value: T }
| { ok: false; error: E };
後端 Service 層:
import type { Result } from "../types/result";
import type { ApiError } from "../types/api";
import { prisma } from "../lib/prisma";
import type { User } from "@prisma/client";
export async function getUser(id: string): Promise<Result<User, ApiError>> {
const user = await prisma.user.findUnique({ where: { id } });
if (!user) {
return {
ok: false,
error: { code: "USER_NOT_FOUND", message: "User not found" },
};
}
return { ok: true, value: user };
}
好處:
if (res.ok)
分支假設 API 錯誤統一為 ApiError
格式,前端就可以這樣處理:
import type { ApiError } from "../types/api";
import type { User } from "../types/user";
async function fetchUser(id: string): Promise<User | ApiError> {
const res = await fetch(`/users/${id}`);
if (!res.ok) {
return (await res.json()) as ApiError;
}
return (await res.json()) as User;
}
async function showUser(id: string) {
const result = await fetchUser(id);
if ("code" in result) {
// result 是 ApiError
console.error(result.code, result.message);
} else {
// result 是 User
console.log(result.name);
}
}
避免 API 格式被後端或網路異常搞壞,可以用 zod
在前端驗證:
import { z } from "zod";
const userSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
});
const apiErrorSchema = z.object({
code: z.string(),
message: z.string(),
details: z.any().optional(),
});
async function fetchUserSafe(id: string) {
const res = await fetch(`/users/${id}`);
const json = await res.json();
if (!res.ok) {
return apiErrorSchema.parse(json); // 失敗就丟錯
}
return userSchema.parse(json);
}
這樣:
User
型別,且已驗證Result
+ zod
終極型別安全type UserResult = Result<
z.infer<typeof userSchema>,
z.infer<typeof apiErrorSchema>
>;
async function fetchUserResult(id: string): Promise<UserResult> {
const res = await fetch(`/users/${id}`);
const json = await res.json();
if (!res.ok) {
return { ok: false, error: apiErrorSchema.parse(json) };
}
return { ok: true, value: userSchema.parse(json) };
}
// 呼叫方:
const result = await fetchUserResult("abc");
if (result.ok) {
console.log(result.value.name);
} else {
console.error(result.error.code);
}
Error Class 型別化
class AppError extends Error {
constructor(
public code: string,
public status: number,
public details?: unknown
) {
super(code);
}
}
錯誤碼列舉
export enum ErrorCode {
UserNotFound = "USER_NOT_FOUND",
EmailExists = "EMAIL_EXISTS",
}
HTTP 錯誤碼與型別對應
type HttpStatus = 400 | 401 | 403 | 404 | 500;